# Copyright 2014 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tool for converting Google Code issues to a format accepted by BitBucket.

Most BitBucket concepts map cleanly to their Google Code equivalent, with the
exception of the following:
 - Issue Assignee is called an Owner
 - Issue Reporter is called an Author
 - Comment User is called an Author
"""

import ssl
if hasattr(ssl, '_create_unverified_context'):
    ssl._create_default_https_context = ssl._create_unverified_context

import argparse
import json
import sys
import urllib
import urllib2
from os.path import isfile

import issues


def _getMilestone(milestone):
  mapping = {
    "release0.2": "0.2",
    "release0.4": "0.4",
    "release0.6": "0.6",
    "release0.8": "0.8",
    "release0.10": "0.10",
    "stauton0.10": "0.10",
    "stauton": "0.10",
    "release0.12": "0.12",
    "anderssen0.12": "0.12",
    "post-0.10": "0.12",
    "post-0.12": "0.14",
    "release1.0": "1.0",
    "release2.0": "2.0",
    "release3.0": "3.0"
  }
  return mapping.get(milestone.lower(), None)


def _getKind(kind):
  mapping = {
    "defect": "bug",
    "enhancement": "enhancement",
    "task": "task",
    "other": "task",
  }
  return mapping.get(kind.lower(), "bug")


def _getPriority(priority):
  mapping = {
    "low": "trivial",
    "medium": "minor",
    "high": "major",
    "critical": "critical",
  }
  return mapping.get(priority.lower(), "minor")


def _getStatus(status):
  mapping = {
      "new": "new",
      "accepted": "open",
      "reopened": "open",
      "started": "open",
      "need-info": "open",
      "need-split": "open",
      "fix-known": "open",
      "fixed": "resolved",
      "verified": "resolved",
      "invalid": "invalid",
      "duplicate": "duplicate",
      "moved": "duplicate",
      "not-reproducible": "wontfix",
      "wontfix": "wontfix",
  }
  return mapping.get(status.lower(), "new")

    
def _getTitle(title):
  if len(title) < 255:
    return title
  return title[:250] + "[...]"


class UserService(issues.UserService):
  """BitBucket user operations.
  """

  def IsUser(self, username):
    """Returns wheter a username is a valid user.

    BitBucket does not have a user api, so accept all usernames.
    """
    return True


class IssueService(issues.IssueService):
  """Abstract issue operations.

  Handles creating and updating issues and comments on an user API.
  """
  def __init__(self):
    self._bitbucket_issues = []
    self._bitbucket_comments = []
    self._bitbucket_attachments = []
    self._bitbucket_logs = []

  def GetIssues(self, state="open"):
    """Gets all of the issue for the repository.

    Since BitBucket does not have an issue API, always returns an empty list.

    Args:
      state: The state of the repository can be either 'open' or 'closed'.

    Returns:
      An empty list.
    """
    return []

  def CreateIssue(self, googlecode_issue):
    """Creates an issue.

    Args:
      googlecode_issue: An instance of GoogleCodeIssue

    Returns:
      The issue number of the new issue.

    Raises:
      ServiceError: An error occurred creating the issue.
    """
    bitbucket_issue = {
        "assignee": googlecode_issue.GetOwner(),
        "content": googlecode_issue.GetDescription(),
        "content_updated_on": googlecode_issue.GetContentUpdatedOn(),
        "created_on": googlecode_issue.GetCreatedOn(),
        "id": googlecode_issue.GetId(),
        "kind": _getKind(googlecode_issue.GetKind()),
        "component": googlecode_issue.GetComponent(),
        "milestone": _getMilestone(googlecode_issue.GetMilestone()),
        "priority": _getPriority(googlecode_issue.GetPriority()),
        "reporter": googlecode_issue.GetAuthor(),
        "status": _getStatus(googlecode_issue.GetStatus()),
        "title": _getTitle(googlecode_issue.GetTitle()),
        "updated_on": googlecode_issue.GetUpdatedOn(),
        "watchers": googlecode_issue.GetWatchers(),
    }
    self._bitbucket_issues.append(bitbucket_issue)
    return googlecode_issue.GetId()

  def CloseIssue(self, issue_number):
    """Closes an issue.

    Args:
      issue_number: The issue number.
    """

  def CreateComment(self, issue_number, source_issue_id,
                    googlecode_comment, project_name,
                    attachments_path, fake_attachments):
    """Creates a comment on an issue.

    Args:
      issue_number: The issue number.
      source_issue_id: The Google Code issue id.
      googlecode_comment: An instance of GoogleCodeComment
      project_name: The Google Code project name.
    """
    author = googlecode_comment.GetAuthor()
    created = googlecode_comment.GetCreatedOn()
    commentId = len(self._bitbucket_comments)
    issueId = googlecode_comment.GetIssue().GetId()
    
    # Comment #0 goes into issue content
    # but it may have attachments!
    if googlecode_comment.GetId() > 0:
        bitbucket_comment = {
            "content": googlecode_comment.GetDescription(),
            "created_on": created,
            "id": commentId,
            "issue": issueId,
            "updated_on": googlecode_comment.GetUpdatedOn(),
            "user": author
        }
        self._bitbucket_comments.append(bitbucket_comment)
    
    attachments = googlecode_comment.GetAttachments()
    if attachments is not None:
        urls = {}
        need_open = False
        for fileName, attachmentId in attachments:
            if fileName:
                if fake_attachments:
                    with open("%s/%s" % (attachments_path, attachmentId), "w") as f:
                        f.write("%s" % fileName.encode("utf-8"))

                if isfile("%s/%s" % (attachments_path, attachmentId)):
                    urls[attachmentId] = (fileName, attachmentId, False)
                else:
                    urls[attachmentId] = (fileName, attachmentId, True)
                    need_open = True
        
        if need_open and not fake_attachments:
            issueUrl = "https://code.google.com/%s/%s/issues/detail?id=%s" % (project_name[0], project_name, issueId)
            data = urllib2.urlopen(issueUrl)
            
            for line in data:
                line = line.strip()
                try:
                    for fileName, attachmentId in attachments:
                        if urls[attachmentId][2]:
                            prefix = '<a href="//%s.googlecode.com/issues/attachment?aid=%s' % (project_name, attachmentId)
                            if line.startswith(prefix) and line.endswith('">'):
                                urls[attachmentId] = (fileName, attachmentId, "https:" + line[9:-2])
                except UnicodeDecodeError:
                    pass

        for id in urls:
            fileName, attachmentId, url = urls[id]
            try:
                if type(url) is str:
                    url = url.replace("&amp;", "&")
                    urllib.urlretrieve (url, "%s/%s" % (attachments_path, attachmentId))

                if type(url) is str or not url:
                    bitbucket_attachment = {
                        "filename": fileName,
                        "issue": issueId,
                        "path": "%s/%s" % (attachments_path, attachmentId),
                        "user": author
                    }
                    self._bitbucket_attachments.append(bitbucket_attachment)
                    
                    if googlecode_comment.GetId() > 0:
                        bitbucket_log = {
                            "changed_from": None,
                            "changed_to": fileName,
                            "comment": commentId,
                            "created_on": created,
                            "field": "attachment",
                            "issue": issueId,
                            "user": author,
                        }
                        self._bitbucket_logs.append(bitbucket_log)
            except:
                print "Failed to download %s for issue #%s" % (url, issueId)
                print "---", attachmentId, fileName
                
    updates = googlecode_comment.GetUpdates()
    if updates is not None:
        if "status" in updates:
            bitbucket_log = {
                "changed_from": None,
                "changed_to": _getStatus(updates["status"]),
                "comment": commentId,
                "created_on": created,
                "field": "status",
                "issue": issueId,
                "user": author,
            }
            self._bitbucket_logs.append(bitbucket_log)

        if "summary" in updates:
            bitbucket_log = {
                "changed_from": None,
                "changed_to": updates["summary"],
                "comment": commentId,
                "created_on": created,
                "field": "title",
                "issue": issueId,
                "user": author,
            }
            self._bitbucket_logs.append(bitbucket_log)

        if "owner" in updates:
            bitbucket_log = {
                "changed_from": None,
                "changed_to": "" if updates["owner"].startswith("-") else updates["owner"],
                "comment": commentId,
                "created_on": created,
                "field": "assignee",
                "issue": issueId,
                "user": author,
            }
            self._bitbucket_logs.append(bitbucket_log)

        if "labels" in updates:
            for label in updates["labels"]:
                removing = label.startswith("-")
                if removing:
                    label = label[1:]
                    
                if label.startswith("Type-"):
                    field, value = label.split("-", 1)
                    field = "kind"
                    value = _getKind(value)
                elif label.startswith("Component-"):
                    field, value = label.split("-", 1)
                    value = value.replace("Component-", "")
                elif label.startswith("Milestone-"):
                    field, value = label.split("-", 1)
                    value = _getMilestone(value)
                elif label.startswith("Priority-"):
                    field, value = label.split("-", 1)
                    value = _getPriority(value)
                else:
                    field = "component"
                    value = label.replace("Component-", "")

                if removing:
                    changed_from = value
                    changed_to = ""
                else:
                    changed_from = None
                    changed_to = value
                    
                bitbucket_log = {
                    "changed_from": changed_from,
                    "changed_to": changed_to,
                    "comment": commentId,
                    "created_on": created,
                    "field": field.lower(),
                    "issue": issueId,
                    "user": author,
                }
                self._bitbucket_logs.append(bitbucket_log)
            
                
  def WriteIssueData(self, default_issue_kind):
    """Writes out the json issue and comments data to db-1.0.json.
    """
    issues_data = {
        "issues": self._bitbucket_issues,
        "comments": self._bitbucket_comments,
        "attachments": self._bitbucket_attachments,
        "logs": self._bitbucket_logs,
        "meta": {"default_kind": default_issue_kind,},
        "milestones": [
            {"name": "0.2"},
            {"name": "0.4"},
            {"name": "0.6"},
            {"name": "0.8"},
            {"name": "0.10"},
            {"name": "0.12"},
            {"name": "0.14"},
            {"name": "1.0"},
            {"name": "2.0"},
            {"name": "3.0"},
            ],
        "components": [{"name": label.replace("Component-", "")} for label in issues.COMPONENT_LABELS],
    }
    with open("db-1.0.json", "w") as issues_file:
      issues_json = json.dumps(issues_data, sort_keys=True, indent=4,
                               separators=(",", ": "))
      issues_file.write(issues_json)


def ExportIssues(issue_file_path, project_name,
                 user_file_path, default_issue_kind, default_username,
                 attachments_path, fake_attachments):
  """Exports all issues for a given project.
  """
  issue_service = IssueService()
  user_service = UserService()

  issue_data = issues.LoadIssueData(issue_file_path, project_name)
  user_map = issues.LoadUserData(user_file_path, default_username, user_service)

  issue_exporter = issues.IssueExporter(
      issue_service, user_service, issue_data, project_name, user_map,
      attachments_path, fake_attachments)

  try:
    issue_exporter.Init()
    issue_exporter.Start()
    issue_service.WriteIssueData(default_issue_kind)
    print "\nDone!\n"
  except IOError, e:
    print "[IOError] ERROR: %s" % e
  except issues.InvalidUserError, e:
    print "[InvalidUserError] ERROR: %s" % e


def main(args):
  """The main function.

  Args:
    args: The command line arguments.

  Raises:
    ProjectNotFoundError: The user passed in an invalid project name.
  """
  parser = argparse.ArgumentParser()
  parser.add_argument("--issue_file_path", required=True,
                      help="The path to the file containing the issues from"
                      "Google Code.")
  parser.add_argument("--project_name", required=True,
                      help="The name of the Google Code project you wish to"
                      "export")
  parser.add_argument("--user_file_path", required=True,
                      help="The path to the file containing a mapping from"
                      "email address to bitbucket username")
  parser.add_argument("--default_issue_kind", required=True,
                      help="A non-null string containing one of the following"
                      "values: bug, enhancement, proposal, task. Defaults to"
                      "bug.")
  parser.add_argument("--default_owner_username", required=True,
                      help="The default issue username")
  parser.add_argument("--attachments_path", required=True,
                      help="Where to download attachments")
  parser.add_argument("--fake_attachments", action="store_true",
                      help="Create fake attachments (for testing purpose)")

  parsed_args, _ = parser.parse_known_args(args)

  ExportIssues(
    parsed_args.issue_file_path, parsed_args.project_name,
    parsed_args.user_file_path, parsed_args.default_issue_kind,
    parsed_args.default_owner_username, parsed_args.attachments_path,
    parsed_args.fake_attachments)


if __name__ == "__main__":
  main(sys.argv)
